/*******************************************************************************
 * Copyright (c) 2009 David Harrison.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl-3.0.html
 *
 * Contributors:
 *     David Harrison - initial API and implementation
 ******************************************************************************/
package com.sfs.postcodes.dao;

import java.util.ArrayList;
import java.util.ResourceBundle;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.sql.DataSource;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.dao.IncorrectResultSizeDataAccessException;


/**
 * The Class PostcodeDAOImpl.
 */
public class PostcodeDAOImpl implements PostcodeDAO {

    /** The Constant serialVersionUID. */
    private static final long serialVersionUID = 1L;

    /** The jdbc template. */
    private JdbcTemplate jdbcTemplate = null;

    /** The _newzealand postcode. */
    private String newzealandPostcode = "";

    /** The _international postcode. */
    private String internationalPostcode = "";

    /** The data logger. */
    private final Logger dataLogger = Logger.getLogger(PostcodeDAO.class);


    /**
     * Sets the data source.
     *
     * @param dataSource the new data source
     */
    public final void setDataSource(final DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    /**
     * Takes an address formatted in the following manner
     * street,suburb,city,state,country and checks for a postcode.
     * Intended to work for New Zealand and Australian addresses.
     *
     * @param address the address
     * @param state the state
     * @param country the country
     * @return the string
     * @throws PostcodeLookupException the postcode lookup exception
     */
    public final String findPostcode(final String address, final String state,
            final String country) throws PostcodeLookupException {

        String postCode = "";

        if (address == null) {
            throw new PostcodeLookupException("Sorry the address string cannot "
                    + "be null when performing a postcode lookup");
        }
        if (country == null) {
            throw new PostcodeLookupException("Sorry the country string cannot "
                    + "be null when performing a postcode lookup");
        }
        if (state == null) {
            throw new PostcodeLookupException("Sorry the state string cannot "
                    + "be null when performing a postcode lookup");
        }

        // Load the SQL strings
        try {
            ResourceBundle sqlBundle = ResourceBundle
                .getBundle("com.sfs.postcodes.dao.sql");

            this.newzealandPostcode = sqlBundle
                .getString("postcode.newzealand");
            this.internationalPostcode  = sqlBundle
                .getString("postcode.international");

        } catch (Exception e) {
            dataLogger.error("Error loading postcode SQL strings: "
                    + e.getMessage());
        }

        ArrayList<String> addressArray = new ArrayList<String>();

        // Replace any instances of a carriage return with a comma
        String addressString = StringUtils.replace(address, "\n", ",");

        StringTokenizer st = new StringTokenizer(addressString, ",");
        while (st.hasMoreTokens()) {
            String component = st.nextToken().trim();
            addressArray.add(component);
        }

        if (country.compareToIgnoreCase("New Zealand") == 0) {
            postCode = findNewZealandPostcode(addressArray);
        }

        if (country.compareToIgnoreCase("Australia") == 0
                && state.compareTo("") != 0) {
            postCode = findInternationalPostcode(addressArray, state, country);
        }
        return postCode;
    }


    /**
     * Find new zealand postcode.
     *
     * @param addressArray the address array
     * @return the string
     * @throws PostcodeLookupException the postcode lookup exception
     */
    private String findNewZealandPostcode(final ArrayList<String> addressArray)
            throws PostcodeLookupException {
        // Perform a postcode search for an Australian address
        String postCode = "";

        try {
            // Loop through the address array performing a search
            // for a postcode. Begin with a tight address and zoom out
            // with the intention of getting the most precise postcode.

            for (int a = 0; a < addressArray.size(); a++) {

                for (int b = addressArray.size(); b > 0; b--) {

                    // If a postcode has been found do not perform the search
                    if (postCode.compareTo("") == 0) {
                        StringBuffer sb = new StringBuffer();
                        // Build search string
                        for (int y = 0; y < b; y++) {

                            int key = y + a;
                            if (key < b) {
                                if (sb.length() > 0) {
                                    sb.append(",");
                                }
                                String searchComponent = addressArray.get(key)
                                        .trim().toUpperCase();
                                searchComponent = StringUtils.replace(
                                        searchComponent, "P.O. ", "PO ");
                                searchComponent = StringUtils.replace(
                                        searchComponent, "P.O.", "PO ");
                                searchComponent = StringUtils.replace(
                                        searchComponent, "Post Office ", "PO ");
                                searchComponent = StringUtils.replace(
                                        searchComponent, "R.D. ", "R D ");
                                searchComponent = StringUtils.replace(
                                        searchComponent, "R.D.", "R D ");
                                searchComponent = StringUtils.replace(
                                        searchComponent, "Rural Delivery ", "R D ");

                                sb.append(searchComponent);
                            }
                        }
                        postCode = performPostCodeLookup(sb.toString());
                    }
                }
            }
        } catch (Exception e) {
            dataLogger.error("Error performing New Zealand postcode search: "
                    + e.getMessage());
        }
        return postCode;
    }


    /**
     * Find international postcode.
     *
     * @param addressArray the address array
     * @param state the state
     * @param country the country
     * @return the international postcode as a string
     * @throws PostcodeLookupException the postcode lookup exception
     */
    private String findInternationalPostcode(
            final ArrayList<String> addressArray, final String state,
            final String country) throws PostcodeLookupException {
        // Perform a postcode search for an Australian address
        String postCode = "";

        try {
            // If a postcode has already been found do not repeat the search
            if (StringUtils.isBlank(postCode)) {
                // Now loop through the address array performing a
                // search for a postcode. Begin broad and zoom in with
                // the intention of getting the most precise postcode.

                for (int i = addressArray.size(); i > 0; i--) {
                    try {
                        postCode = (String) this.jdbcTemplate.queryForObject(
                            this.internationalPostcode,
                            new Object[]{
                                addressArray.get(i - 1).trim().toUpperCase(),
                                state.toUpperCase(),
                                country.toUpperCase()
                            },
                            new RowMapper() {
                                public Object mapRow(final ResultSet rs, final int rowNum)
                                        throws SQLException {
                                    return rs.getString("postcode");
                                }
                            });

                        dataLogger.info("Postcode: " + postCode);

                    } catch (IncorrectResultSizeDataAccessException ie) {
                        // No results found for this search - keep looking
                        dataLogger.debug("No search results found");
                    }
                }
            }
        } catch (Exception e) {
            dataLogger.error("Error performing international postcode search: "
                    + e.getMessage());
        }
        return postCode;
    }


    /**
     * Perform post code lookup.
     *
     * @param address the address
     * @return the postcode as a string
     */
    private String performPostCodeLookup(final String address) {

        String postCode = "";
        // If the search string is empty do not perform the search
        if (address != null && address.length() > 0) {

            dataLogger.debug("Looking up address: " + address);

            postCode = performJdbcSearch(address);

            dataLogger.debug("Postcode: " + postCode);

            if (postCode.length() == 0) {

                dataLogger.debug("No address found on first pass");

                // No postcode found, check to see if it begins with a number

                // Create a pattern to addresses beginning with a number
                Pattern pattern = Pattern.compile("^\\d");
                Matcher match = pattern.matcher(address);

                if (match.lookingAt()) {
                    dataLogger.debug("The address begins with a number");

                    // If a match perform a search based on the address string
                    // minus the starting number.
                    try {
                        final String clippedAddress = address.substring(
                                address.indexOf(" ") + 1, address.length());

                        dataLogger.debug("Revised address: " + clippedAddress);

                        postCode = performJdbcSearch(clippedAddress);
                        dataLogger.debug("Postcode: " + postCode);

                    } catch (Exception e) {
                        dataLogger.error("Error performing postcode (decimal) "
                                + "search: " + e.getMessage());
                    }
                }
            }
        }
        return postCode;
    }


    /**
     * Perform jdbc search.
     *
     * @param address the address
     * @return the search result as a string
     */
    private String performJdbcSearch(final String address) {
        String postCode = "";
        try {
            postCode = (String) this.jdbcTemplate.queryForObject(
                this.newzealandPostcode,
                new Object[]{address + "%"},
                new RowMapper() {
                    public Object mapRow(final ResultSet rs, final int rowNum)
                            throws SQLException {
                        return rs.getString("postcode");
                    }
                });

            dataLogger.info("Postcode: " + postCode);

        } catch (IncorrectResultSizeDataAccessException ie) {
            // No results found for this search - keep looking
            dataLogger.debug("No postcode found in search");
        }
        return postCode;
    }
}

